VNCTF2022可把我打惨了。摆烂的结果就是这样的。开始好好学了(不我还是去打《底特律》吧)
更新:摸鱼摸到3号才写完,我无语了。
Go逆向_1
简要
Go的逆向还是要比Cython
逆向好多了。Cython
的内联简直要人命。
学习要点一切以加速逆向速度为核心。
谈谈我打算学习的知识。
- Go语言基础(不写博客)
- 一些小技巧
- Go语言底层内存模型
- Go程序结构
环境
1 | go version go1.16.4 windows/amd64 |
现在最高版本应该是1.17.7,但是各个版本之间的区别以后再说。
Go程序的初始化
入口函数是很重要的一环。
rt0_amd64_windows
保留符号表的Go程序,用IDA反编译后并不能在函数栏里面很快的发现入口点。但是通过扔进DIE或者其他识别工具,我们知道第一个入口函数是rt0_amd64_windows
。
1 | // attributes: thunk |
然后调用rt0_amd64()
rt0_amd64
1 | __int64 __usercall rt0_amd64@<rax>() |
rt0_go
然后调用rt0_go()
。这是一个核心函数。
1 | __int64 __usercall runtime_rt0_go@<rax>() |
在runtime/asm_amd64.s
中有_rt0_amd64()
和rt0_go
的汇编。
1 | // _rt0_amd64 is common startup code for most amd64 systems when using |
通过RDI
和RSI
接收到参数个数以及参数所在地址后,跳到runtime.rt0_go
函数。
1 | // Defined as ABIInternal since it does not use the stack-based Go ABI (and |
首先这个函数原型表明是无参函数。
1 | TEXT runtime·rt0_go<ABIInternal>(SB),NOSPLIT,$0 |
然后在栈上传入argc
和argv
。
1 | // copy arguments forward on an even stack |
然后初始化g0
执行栈。g0
是一个特殊的Goroutine
。Go语言一个很重要的特性就是协程,通过多个Go协程来充分的运用多核CPU中每一个核心的性能。其中g0
是为每个OS线程创建的第一个Goroutine
,用于调度其他运行着的线程上的Goroutine
。
具体可以看 https://medium.com/a-journey-with-go/go-g0-special-goroutine-8c778c6704d8 来了解。
不过里面作者还需要你去了解Go中M, P, G
模型的原理。文章里面附有链接。
然后调用cpuid
指令了解当前CPU的信息。
1 | // find out information about the processor we're on |
然后想办法如何将RDTSC
序列化。rdtsc
指令是返回处理器自开机其的时钟周期的。
1 | // Figure out how to serialize RDTSC. |
关于“序列化”,首先得知道现代CPU基本上都是乱序执行的,通过将多个指令打乱随机并行执行,这样能提高运行效率。
而序列化的指令,大概的意思就是说其不是乱序执行的,在执行下一条指令之前会拥有CPU和缓存的唯一控制权,而不会和其他指令并行执行。
比较考究这个的就是内存屏障这类指令。
下面是非Intel处理器或cpuid
指令无效时的应对措施。
1 | notintel: |
调整调用规范,改为r9
, r8
, dx
, cx
寄存器。
1 | #ifdef GOOS_windows |
然后调用AX
寄存器,来调用runtime.bss()
函数(汇编里面是_cgo_init()
但是在IDA里面变了)
1 | CALL AX |
设置stackguard
1 | // update stackguard after _cgo_init |
设置TLS。
TLS(Thread Local Storage,局部线程存储)是一种变量的存储方法,这个变量在它所在的线程内是全局可访问的,但是不能被其他线程访问到,这样就保持了数据的线程独立性。 而熟知的全局变量,是所有线程都可以访问的,这样就不可避免需要锁来控制,增加了控制成本和代码复杂度。
https://zhuanlan.zhihu.com/p/142418922
不同的操作系统,编译器都有自己的实现方法。
https://zh.wikipedia.org/wiki/%E7%BA%BF%E7%A8%8B%E5%B1%80%E9%83%A8%E5%AD%98%E5%82%A8
https://docs.microsoft.com/en-us/cpp/parallel/thread-local-storage-tls?view=msvc-170
1 | LEAQ runtime·m0+m_tls(SB), DI |
为每个goroutine
设置寄存器内容
1 | ok: |
然后调用runtime.check()
该函数在runtime/runtime1.go/check()
,进行各种检查,包括类型的长度Sizeof、结构体字段的偏移量、CAS操作、指针操作、原子操作、汇编指令、栈大小检查等
1 | CLD // convention is D is always left cleared |
然后
1 | MOVL 16(SP), AX // copy argc |
然后运行一个新的goroutine
开始主程序(从runtime.main
开始)的运行。
1 | // create a new goroutine to start program |
其中,newproc
压入的第一个参数是命令行参数大小,第二个是mainPC
指针,指向了runtime.main
函数。
1 | // start this M |
总结一下:
- 复制参数到栈上
- 初始化
g0
执行栈 - 使用
cpuid
指令获取CPU信息 - 序列化
RDTSC
指令 - 设置
TLS
局部线程存储 - 为每个
goroutine
初始化寄存器内容 - 调用
runtime.check()
检查各种信息 - 调用
args()
初始化命令行参数 - 调用
osinit()
读取CPU核数 - 调用
schedinit()
调度器初始化 - 创建新的
goroutine
,启动主程序
[go runtime] - go程序启动过程 - 掘金 (juejin.cn)
后面还详细讲了一下args
,osinit
等函数的过程。这里不再详细分析,关注于最后的runtime·newproc(SB)
函数
newproc
1 | // 创建一个运行`fn`函数的goroutine,且带有siz字节大小的参数 |
anyway,创建好这个之后,runtime.main()
所代表的goroutine
就被放在队列中,等待mstart
依次执行了。
mstart
1 | // mstart is the entry-point for new Ms. |
可以看到这里依次执行了一大堆函数,包括fn()
函数。
然后到了runtime.main
函数了。这个函数内直接调用到了main.main
主函数。不过由于是call rbx
这类非直接调用,所以在strip
后的程序中会出现一个奇异的函数指针调用,那个就是main.main
函数的位置。具体的定位方式在技巧中会讲。
技巧
快速定位main函数
这是一开始用IDA7.5导致没法完美恢复函数表,被逼无奈学出来的技巧。
当然并不是说恢复符号表就是万能的,因为有题目会混淆函数字符串,使得你恢复后的反而更难以分析。
例子
先写个例子
1 | // hello.go |
使用
1 | go build hello.go |
编译
得到hello.exe
。
这种情况下是含有符号表信息的。任意版本的IDA皆可直接直接定位到main_main
函数。
下面剥去符号表。
1 | strip .\hello.exe -o hello_stripped.exe |
然后用IDA7.5以下版本打开hello_stripped.exe
。
此时就会发现开局就是一大串无效数据,没法定位。
尝试性的定位start
函数,但是实际上这个函数与Go的main_main
函数毫无干系。
比对有符号表和无符号表的程序,可以发现无符号表的start
函数其实是
1 | void __golang rt0_amd64_windows(char a1) |
且可以发现此函数和main_main
函数并没有任何调用关系。 (肯定有关系但是对于初学者来说,不是很清晰)
正确方法
main_main
先会被runtime_main
函数调用
1 | void runtime_main() |
可以发现下面有一串
1 | while(1) |
的错误代码(IDA中会显著标红)
这一块已经在runtime_exit(0)
的后面了,一般是不会触发的。否则就会到这里触发异常处理。
看此处汇编
1 | xor eax, eax |
字节码是
1 | 31 C0 |
根据搜寻,整个Go程序中只有这里会有这样的逻辑。所以想要定位main
函数,只需要搜索这几个字节码即可。
然后找到所在地址,发现大概率还处于一个被IDA误认为是数据块的地方。
然后转成代码,设置为函数,这样runtime_main
就被定位了。
反编译得到伪代码,检查最后一小块
1 | sub_4057A0(); |
其中
1 | ((void (__fastcall *)(__int64, void **))&qword_4A7728[87])(v2, &off_4D6068); |
就是对main_main
函数的调用。
看汇编
1 | .text:00000000004387E6 mov rax, cs:off_4D6068 |
所以off_4D6068
就是main_main
函数入口点。
- 本文作者: Taardis
- 本文链接: https://taardisaa.github.io/2022/02/17/Go Reverse 1/
- 版权声明: 本博客所有文章除特别声明外,均采用 Apache License 2.0 许可协议。转载请注明出处!